package resml

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/textproto"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const (
	META_CONTENT_LENGTH = "Content-Length" // 内容长度，请求+响应
	META_CONTENT_TYPE   = "Content-Type"   // 内容类型，请求+响应
	META_LAST_MODIFIED  = "Last-Modified"  // 最后修改时间，仅响应
	META_ETAG           = "ETag"           // 实例标识符，仅响应
	META_REQ_MODIFIED   = "X-Modified-At"  // 资源在客户端的修改时间，仅请求
	META_REQ_ETAG       = "X-ETag"         // 资源在客户端的实体标识符，仅请求

	MIME_OCTETE_STREAM = "application/octet-stream"
)

// 资源元数据
type Meta interface {
	ContentLength() int64      // 资源内容的字节大小，负数表示未知
	SetContentLength(int64)    // 设置资源的字节大小
	ContentType() string       // 资源的 MIME 类型，可以为空字符串
	SetContentType(string)     // 设置资源的 MIME 类型
	LastModified() time.Time   // 资源的最后修改时间，可以为零值
	SetLastModified(time.Time) // 设置资源的最后修改时间
	ETag() ETag                // 资源的 ETag，ETag 可以为 nil
}

// 资源元数据
type ResourceMeta struct {
	contentLength int64
	contentType   string
	lastModified  time.Time
	etag          ETag
}

// 设置资源内容的字节大小
func (m *ResourceMeta) WithContentLength(v int64) *ResourceMeta {
	m.SetContentLength(v)
	return m
}

// 设置资源的 MIME 类型
func (m *ResourceMeta) WithContentType(v string) *ResourceMeta {
	m.contentType = v
	return m
}

// 设置资源最后修改时间
func (m *ResourceMeta) WithLastModified(v time.Time) *ResourceMeta {
	m.SetLastModified(v)
	return m
}

// 设置资源的 ETag
func (m *ResourceMeta) WithETag(v ETag) *ResourceMeta {
	m.etag = v
	return m
}

// 获取资源内容的字节大小
func (m *ResourceMeta) ContentLength() int64 {
	return m.contentLength
}

// 设置资源的字节大小
func (m *ResourceMeta) SetContentLength(v int64) {
	m.contentLength = v
}

// 获取资源的 MIME 类型
func (m *ResourceMeta) ContentType() string {
	return m.contentType
}

// 设置资源的 MIME 类型
func (m *ResourceMeta) SetContentType(v string) {
	if vv := strings.TrimSpace(v); vv != "" {
		m.contentType = vv
	}
}

// 获取资源最后修改时间
func (m *ResourceMeta) LastModified() time.Time {
	return m.lastModified
}

// 设置资源的最后修改时间
func (m *ResourceMeta) SetLastModified(v time.Time) {
	m.lastModified = v
}

// 获取资源的 ETag
func (m *ResourceMeta) ETag() ETag {
	return m.etag
}

// Meta 的 ETag 与 ContentLength 是否相等
func MetaEtagSizeEquals(a, b Meta) bool {
	if a == nil || b == nil || a == b {
		return a == b
	}
	return a.ContentLength() == b.ContentLength() && ETagEquals(a.ETag(), b.ETag())
}

// 判断 Meta 是否相等(时间的对比精确到秒)
func MetaEquals(a, b Meta) bool {
	if !MetaEtagSizeEquals(a, b) {
		return false
	}
	return a.ContentType() == b.ContentType() && SecondEquals(a.LastModified(), b.LastModified())
}

// 判断时间是否相等(精确到秒)
func SecondEquals(a, b time.Time) bool {
	return a.Unix() == b.Unix()
}

// 写到 HTTP 头。
func MetaSetToHttpHeader(m Meta, h http.Header, isRequest bool) {
	if ct := m.ContentType(); ct != "" {
		h.Set(META_CONTENT_TYPE, ct)
	}
	if size := m.ContentLength(); size >= 0 {
		h.Set(META_CONTENT_LENGTH, fmt.Sprintf("%d", size))
	}
	etn, lmn := META_ETAG, META_LAST_MODIFIED
	if isRequest {
		etn, lmn = META_REQ_ETAG, META_REQ_MODIFIED
	}
	if etag := m.ETag(); etag != nil {
		h.Set(etn, `"`+etag.String()+`"`)
	}
	if tm := m.LastModified(); !tm.IsZero() {
		h.Set(lmn, tm.UTC().Format(http.TimeFormat))
	}
}

// 从 HTTP Header 或 MimeHeader 中获取 Meta
func MetaOfHttpHeader(header http.Header) (meta Meta, err error) {
	if len(header) == 0 {
		return nil, errors.New("nil or empty header")
	}
	fn := func(call func(val string) error, keys ...string) error {
		for _, key := range keys {
			if v := header.Get(key); v != "" {
				return call(v)
			}
		}
		return nil
	}
	m := &ResourceMeta{contentType: header.Get(META_CONTENT_TYPE), contentLength: -1}
	if err = fn(func(val string) error {
		cl, err := strconv.ParseInt(val, 10, 64)
		if err == nil {
			m.contentLength = cl
		}
		return err
	}, META_CONTENT_LENGTH); err != nil {
		return nil, err
	}
	if err = fn(func(val string) error {
		et, err := ETagParse(val)
		if err == nil {
			m.etag = et
		}
		return err
	}, META_ETAG, META_REQ_ETAG); err != nil {
		return nil, err
	}
	if err = fn(func(val string) error {
		lm, err := http.ParseTime(val)
		if err == nil {
			m.lastModified = lm
		}
		return err
	}, META_LAST_MODIFIED, META_REQ_MODIFIED); err != nil {
		return nil, err
	}
	return m, nil
}

// 从 reader 中读取 HTTP 头部格式的文本，并解析出 Meta
func MetaOfHttpHeaderTextReader(reader io.Reader) (Meta, error) {
	hd, err := textproto.NewReader(bufio.NewReader(reader)).ReadMIMEHeader()
	if err != nil && err != io.EOF {
		return nil, err
	}
	return MetaOfHttpHeader(http.Header(hd))
}

// 从指定文件中读取 HTTP 头部格式的文本，并解析出 Meta
func MetaOfHttpHeaderTextFile(fn string) (Meta, error) {
	fd, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	defer fd.Close()
	return MetaOfHttpHeaderTextReader(fd)
}

// 读取文件，根据文件生成元数据
func MetaOfFile(fn string, etager ETager) (Meta, error) {
	fd, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	defer fd.Close()
	stat, err := fd.Stat()
	if err != nil {
		return nil, err
	}
	return MetaOfReader(fd, etager, stat.ModTime(), filepath.Ext(fn))
}

// 读取文件，根据文件生成元数据，但忽略 ETag （返回的 Meta 中的 ETag 为 nil）
func MetaOfFileWithoutETag(fn string) (*ResourceMeta, error) {
	fd, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	defer fd.Close()
	stat, err := fd.Stat()
	if err != nil {
		return nil, err
	}
	meta := &ResourceMeta{contentLength: stat.Size(), lastModified: stat.ModTime()}
	buf := make([]byte, 512)
	io.ReadFull(fd, buf)
	meta.contentType = mimeTypeDetect(buf, filepath.Ext(fn))
	return meta, nil
}

// 从 reader 读取内容，根据内容及或后缀名（extName，例如 .jpg）生成元数据。
// 注意：MetaOfReader 会从头到尾读取 reader，所以要注意 reader 是否可重复读或一次性读。
func MetaOfReader(reader io.Reader, etager ETager, lastModified time.Time, extName string) (Meta, error) {
	return MetaOfCopy(io.Discard, reader, etager, lastModified, extName)
}

// 从 reader 读取内容，根据内容及或后缀名（extName，例如 .jpg）生成元数据。
// 注意：MetaOfReader 会从头到尾读取 reader，所以要注意 reader 是否可重复读或一次性读。
func MetaOfCopy(writer io.Writer, reader io.Reader, etager ETager, lastModified time.Time, extName string) (*ResourceMeta, error) {
	// 用于接收前 512 bytes 来推断 mime-type。
	mb := new(bytes.Buffer)
	// 计算 ETag：
	// 1. 将 reader 复制到 writer 以实现存储或输出等目的
	// 2. 从 reader 读取前 512 字节，计算 MimeType
	// 3. 将 reader 写入到 hash.Hash 计算 Hash 值以生成 ETag
	etag, n, err := etager.OfCopy(io.MultiWriter(writer, NewLimitedWriter(mb, 512)), reader)
	if err != nil {
		return nil, err
	}
	return &ResourceMeta{
		contentType:   mimeTypeDetect(mb.Bytes(), extName),
		contentLength: n,
		lastModified:  lastModified,
		etag:          etag,
	}, nil
}

func mimeTypeDetect(buf []byte, ext string) string {
	mt := http.DetectContentType(buf)
	if (mt == MIME_OCTETE_STREAM || mt == "") && ext != "" {
		mt = mime.TypeByExtension(ext)
	}
	return mt
}

func metaFix(inputed Meta, computed *ResourceMeta) Meta {
	if computed == nil {
		return inputed
	}
	if inputed == nil || (inputed.ETag() == nil && inputed.ContentType() == "") {
		return computed
	}
	if ct, rt := inputed.ContentType(), computed.ContentType(); ct != "" && (rt == "" || rt == MIME_OCTETE_STREAM) {
		computed.contentType = ct // 计算值 computed 更加准确，所以计算值为空或默认值时才设置
	}
	if cl, rl := inputed.ContentLength(), computed.ContentLength(); cl > 0 && rl <= 0 {
		computed.contentLength = cl // 计算值 computed 的长度更准确，所以计算值非法时才设置
	}
	if me, re := inputed.ETag(), computed.ETag(); me != nil && re == nil {
		computed.etag = me // 计算值 computed 的 ETag 更准确，所以计算值为空时才设置
	}
	if t := inputed.LastModified(); !t.IsZero() {
		computed.lastModified = t // 输入值 inputed 的 LastModified 更准确
	}
	return computed
}

// 保存资源内容，并通过资源内容，修复 Meta 中缺失的信息。
func MetaFix(w io.Writer, r io.Reader, etager ETager, meta Meta, mt time.Time, ext string) (Meta, error) {
	ret, err := MetaOfCopy(w, r, etager, mt, ext)
	if err != nil {
		return ret, err
	}
	return metaFix(meta, ret), nil
}

// 将 Meta 转为 HTTP 头部格式的文本
func MetaHttpHeaderText(meta Meta) string {
	if meta == nil {
		return ""
	}
	sb := new(strings.Builder)
	if ct := meta.ContentType(); ct != "" {
		fmt.Fprintf(sb, "%s: %s\r\n", META_CONTENT_TYPE, ct)
	}
	if size := meta.ContentLength(); size >= 0 {
		fmt.Fprintf(sb, "%s: %s\r\n", META_CONTENT_LENGTH, fmt.Sprintf("%d", size))
	}
	if etag := meta.ETag(); etag != nil {
		fmt.Fprintf(sb, "%s: %s\r\n", META_ETAG, etag.String())
	}
	if tm := meta.LastModified(); !tm.IsZero() {
		fmt.Fprintf(sb, "%s: %s\r\n", META_LAST_MODIFIED, tm.UTC().Format(http.TimeFormat))
	}
	return sb.String()
}
